package ij.plugin;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.Animator;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;

/** <pre>
 * ImageJ Plugin for reading an AVI file into an image stack
 *  (one slice per video frame)
 * 
 *  Version 2008-07-03 by Michael Schmid, based on a plugin by
 *  Daniel Marsh and Wayne Rasband
 *
 * Restrictions and Notes:
 *      - Only few formats supported:
 *          - uncompressed 8 bit with palette (=LUT)
 *          - uncompressed 8 & 16 bit grayscale
 *          - uncompressed 24 & 32 bit RGB (alpha channel ignored)
 *          - uncompressed 32 bit AYUV (alpha channel ignored)
 *          - various YUV 4:2:2 compressed formats
 *          - png or jpeg-encoded individual frames.
 *            Note that most MJPG (motion-JPEG) formats are not read correctly.
 *      - Does not read avi formats with more than one frame per chunk
 *      - Palette changes during the video not supported
 *      - Out-of-sequence frames (sequence given by index) not supported
 *      - Different frame sizes in one file (rcFrame) not supported
 *      - Conversion of (A)YUV formats to grayscale is non-standard:
 *        All 255 levels are kept as in the input (i.e. the full dynamic
 *        range of data from a frame grabber is preserved).
 *        For standard behavior, use "Brightness&Contrast", Press "Set",
 *        enter "Min." 16, "Max." 235, and press "Apply".
 * Version History:
 *   2008-07-03
 *      - Support for 16bit AVIs coded by MIL (Matrox Imaging Library)
 *   2008-06-08
 *      - Support for png and jpeg/mjpg encoded files added
 *      - Retrieves animation speed from image frame rate
 *      - Exception handling without multiple error messages
 *   2008-04-29
 *      - Support for several other formats added, especially some YUV
 *        (also named YCbCr) formats
 *      - Uneven chunk sizes fixed
 *      - Negative biHeight fixed  
 *      - Audio or second video stream don't cause a problem
 *      - Can read part of a file (specify start & end frame numbers)
 *      - Can convert YUV and RGB to grayscale (does not convert 8-bit with palette)
 *      - Can flip vertically
 *      - Can create a virtual stack
 *      - Added slice label: time of the frame in the movie
 *      - Added a public method 'getStack' that does not create an image window
 *      - More compact code, especially for reading the header (rewritten)
 *      - In the code, bitmapinfo items have their canonical names
 *
 * The AVI format looks like this:
 * RIFF                     RIFF HEADER
 * |-AVI                    AVI CHUNK  
 *   |LIST hdrl             MAIN AVI HEADER
 *   | |-avih               AVI HEADER
 *   | |LIST strl           STREAM LIST(s) (One per stream)
 *   | | |-strh             STREAM HEADER (Required after above; fourcc type is 'vids' for video stream)
 *   | | |-strf             STREAM FORMAT (for video: BitMapInfo; may also contain palette)
 *   | | |-strd             OPTIONAL -- STREAM DATA (ignored in this plugin)
 *   | | |-strn             OPTIONAL -- STREAM NAME (ignored in this plugin)
 *   |LIST movi             MOVIE DATA
 *   | | [rec]              RECORD DATA (one record per frame for interleaved video; optional, unsupported in this plugin)
 *   | |  |-dataSubchunks   RAW DATA: '??wb' for audio, '??db' and '??dc' for uncompressed and
 *   |                      compressed video, respectively. "??" denotes stream number, usually "00" or "01"
 *   |-idx1                 AVI INDEX (required by some programs, ignored in this plugin)
 * </pre>
 */

public class AVI_Reader extends VirtualStack implements PlugIn {

    //four-character codes for avi chunk types
    //NOTE: byte sequence is reversed - ints in Intel (little endian) byte order!
    private final static int   FOURCC_RIFF = 0x46464952;   //'RIFF'
    private final static int   FOURCC_AVI =  0x20495641;   //'AVI '
    private final static int   FOURCC_LIST = 0x5453494c;   //'LIST'
    private final static int   FOURCC_hdrl = 0x6c726468;   //'hdrl'
    private final static int   FOURCC_avih = 0x68697661;   //'avih'
    private final static int   FOURCC_strl = 0x6c727473;   //'strl'
    private final static int   FOURCC_strh = 0x68727473;   //'strh'
    private final static int   FOURCC_strf = 0x66727473;   //'strf'
    private final static int   FOURCC_movi = 0x69766f6d;   //'movi'
    private final static int   FOURCC_rec =  0x20636572;   //'rec '
    private final static int   FOURCC_JUNK = 0x4b4e554a;   //'JUNK'
    private final static int   FOURCC_vids = 0x73646976;   //'vids'
    private final static int   FOURCC_00db = 0x62643030;   //'00db'
    private final static int   FOURCC_00dc = 0x63643030;   //'00dc'
    //four-character codes for supported compression formats; see fourcc.org
    private final static int   NO_COMPRESSION    = 0;          //uncompressed, also 'RGB ', 'RAW '
    private final static int   NO_COMPRESSION_RGB= 0x20424752; //'RGB ' -a name for uncompressed
    private final static int   NO_COMPRESSION_RAW= 0x20574152; //'RAW ' -a name for uncompressed
    private final static int   NO_COMPRESSION_Y800=0x30303859;//'Y800' -a name for 8-bit grayscale
    private final static int   NO_COMPRESSION_Y8 = 0x20203859; //'Y8  ' -another name for Y800
    private final static int   NO_COMPRESSION_GREY=0x59455247;//'GREY' -another name for Y800
    private final static int   NO_COMPRESSION_Y16= 0x20363159; //'Y16 ' -a name for 16-bit uncompressed grayscale
    private final static int   NO_COMPRESSION_MIL= 0x204c494d; //'MIL ' - Matrox Imaging Library
    private final static int   AYUV_COMPRESSION  = 0x56555941; //'AYUV' -uncompressed, but alpha, Y, U, V bytes
    private final static int   UYVY_COMPRESSION  = 0x59565955; //'UYVY' - 4:2:2 with byte order u y0 v y1
    private final static int   Y422_COMPRESSION  = 0x564E5955; //'Y422' -another name for of UYVY
    private final static int   UYNV_COMPRESSION  = 0x32323459; //'UYNV' -another name for of UYVY
    private final static int   CYUV_COMPRESSION  = 0x76757963; //'cyuv' -as UYVY but not top-down
    private final static int   V422_COMPRESSION  = 0x32323456; //'V422' -as UYVY but not top-down
    private final static int   YUY2_COMPRESSION  = 0x32595559; //'YUY2' - 4:2:2 with byte order y0 u y1 v
    private final static int   YUNV_COMPRESSION  = 0x564E5559; //'YUNV' -another name for YUY2
    private final static int   YUYV_COMPRESSION  = 0x56595559; //'YUYV' -another name for YUY2
    private final static int   YVYU_COMPRESSION  = 0x55595659; //'YVYU' - 4:2:2 with byte order y0 u y1 v
    private final static int   JPEG_COMPRESSION  = 0x6765706a; //'jpeg' JPEG compression of individual frames
    private final static int   JPEG_COMPRESSION2 = 0x4745504a; //'JPEG' JPEG compression of individual frames
    private final static int   JPEG_COMPRESSION3 = 0x04;       //BI_JPEG: JPEG compression of individual frames
    private final static int   MJPG_COMPRESSION  = 0x47504a4d; //'MJPG' Motion JPEG, also reads compression of individual frames
    private final static int   PNG_COMPRESSION   = 0x20676e70; //'png ' PNG compression of individual frames
    private final static int   PNG_COMPRESSION2  = 0x20474e50; //'PNG ' PNG compression of individual frames
    private final static int   PNG_COMPRESSION3  = 0x05;       //BI_PNG PNG compression of individual frames

    private final static int   BITMASK24 = 0x10000;     //for 24-bit (in contrast to 8, 16,... not a bitmask)
    private final static long  SIZE_MASK = 0xffffffffL; //for conversion of sizes from unsigned int to long

    //dialog parameters are static so they will be remembered
    private static int         firstFrameNumber = 1; //the first frame to read
    private static int         lastFrameNumber = 0;  //the last frame to read; 0 means 'read all'
    private static boolean     convertToGray;        //whether to convert color video to grayscale
    private static boolean     flipVertical;         //whether to flip image vertical
    private static boolean     isVirtual;            //whether to open as virtual stack
    //the input file
    private  RandomAccessFile  raFile;
    private  String            raFilePath;
    private  boolean           headerOK = false;    //whether header has been read
    //more avi file properties etc
    private  int               streamNumber;        //number of the (first) video stream
    private  long              fileSize = 0;        //file size (for progress bar)
    private  int               paddingGranularity = 2;  //tags start at even address
    //derived from BitMapInfo
    private  int               dataCompression;     //data compression type used
    private  int               scanLineSize;
    private  boolean           dataTopDown;         //whether data start at top of image
    private  ColorModel        cm;
    private  boolean           variableLength;      //compressed (PNG, JPEG) frames have variable length
    //for conversion to ImageJ stack
    private  Vector            frameInfos;  //for virtual stack: long[] with frame pos&size in file, time(usec)
    private  ImageStack        stack;
	private  ImagePlus	       imp;
    //for debug messages
    private  boolean           verbose = IJ.debugMode;
    private  long              startTime;


    //From AVI Header Chunk
    private  int               dwMicroSecPerFrame;
    private  int               dwMaxBytesPerSec;
    private  int               dwReserved1;
    private  int               dwFlags;
    private  int               dwTotalFrames;
    private  int               dwInitialFrames;
    private  int               dwStreams;
    private  int               dwSuggestedBufferSize;
    private  int               dwWidth;
    private  int               dwHeight;

    //From Stream Header Chunk
    private  int               fccStreamHandler;
    private  int               dwStreamFlags;
    private  int               dwPriorityLanguage;  //actually 2 16-bit words: wPriority and wLanguage
    private  int               dwStreamInitialFrames;
    private  int               dwStreamScale;
    private  int               dwStreamRate;
    private  int               dwStreamStart;
    private  int               dwStreamLength;
    private  int               dwStreamSuggestedBufferSize;
    private  int               dwStreamQuality;
    private  int               dwStreamSampleSize;

    //From Stream Format Chunk: BITMAPINFO contents (40 bytes)
    private  int               biSize;              // size of this header in bytes (40)
    private  int               biWidth;
    private  int               biHeight;
    private  short             biPlanes;            // no. of color planes: for the formats decoded; here always 1
    private  short             biBitCount;          // Bits per Pixel
    private  int               biCompression;
    private  int               biSizeImage;         // size of image in bytes (may be 0: if so, calculate)
    private  int               biXPelsPerMeter;     // horizontal resolution, pixels/meter (may be 0)
    private  int               biYPelsPerMeter;     // vertical resolution, pixels/meter (may be 0)
    private  int               biClrUsed;           // no. of colors in palette (if 0, calculate)
    private  int               biClrImportant;      // no. of important colors (appear first in palette) (0 means all are important)



    /** The plugin is invoked by ImageJ using this method.
     *  String 'arg' may be used to select the path.
     */
    public void run (String arg) {
        OpenDialog  od = new OpenDialog("Select AVI File", arg);    //file dialog
        String fileName = od.getFileName();
        if (fileName == null) return;
        String fileDir = od.getDirectory();
        String path = fileDir + fileName;
        try {
            openAndReadHeader(path);                                //open and read header
        } catch (Exception e) {
            IJ.showMessage("AVI Reader", exceptionMessage(e));
            return;
        }
        if (!showDialog(fileName)) return;                          //ask for parameters
        try {
            ImageStack stack = makeStack (path, firstFrameNumber, lastFrameNumber,
                    isVirtual, convertToGray, flipVertical);        //read data
        } catch (Exception e) {
            IJ.showMessage("AVI Reader", exceptionMessage(e));
            return;
        }
        if (stack == null) return;
        if (stack.getSize() == 0) {
            String rangeText = "";
            if (firstFrameNumber>1 || lastFrameNumber!=0)
                rangeText = "\nin Range "+firstFrameNumber+
                    (lastFrameNumber>0 ? " - "+lastFrameNumber : " - end");
            IJ.showMessage("AVI Reader","Error: No Frames Found"+rangeText);
            return;
        }
        imp = new ImagePlus(fileName, stack);
        if (imp.getBitDepth()==16)
        	imp.getProcessor().resetMinAndMax();
        setFramesPerSecond(imp);
        FileInfo fi = new FileInfo();
        fi.fileName = fileName;
        fi.directory = fileDir;
        imp.setFileInfo(fi);
        if (arg.equals(""))
        	imp.show();
        IJ.showTime(imp, startTime, "Read AVI in ", stack.getSize());
    }

    /** Returns the ImagePlus opened by run(). */
	public ImagePlus getImagePlus() {
		return imp;
	}

    /** Create an ImageStack from an avi file with given path.
     * @param path              Directoy+filename of the avi file
     * @param firstFrameNumber  Number of first frame to read (first frame of the file is 1)
     * @param lastFrameNumber   Number of last frame to read or 0 for reading all, -1 for all but last...
     * @param isVirtual         Whether to return a virtual stack
     * @param convertToGray     Whether to convert color images to grayscale
     * @return  Returns the stack; null on failure.
     *  The stack returned may be non-null, but have a length of zero if no suitable frames were found
     */
    public ImageStack makeStack (String path, int firstFrameNumber, int lastFrameNumber,
            boolean isVirtual, boolean convertToGray, boolean flipVertical) {
        this.firstFrameNumber = firstFrameNumber;
        this.lastFrameNumber = lastFrameNumber;
        this.isVirtual = isVirtual;
        this.convertToGray = convertToGray;
        this.flipVertical = flipVertical;
        String exceptionMessage = null;
        IJ.showProgress(.001);
        try {
            readAVI(path);
        } catch (OutOfMemoryError e) {
            stack.trim();
            IJ.showMessage("AVI Reader", "Out of memory.  " + stack.getSize() + " of " + dwTotalFrames + " frames will be opened.");
        } catch (Exception e) {
            exceptionMessage = exceptionMessage(e);
        } finally {
            try {
                raFile.close();
                if (verbose)
                    IJ.log("File closed.");
            } catch (Exception e) {}
            IJ.showProgress(1.0);
        }
        if (exceptionMessage != null) throw new RuntimeException(exceptionMessage);
        if (isVirtual && frameInfos != null)
            stack = this;
        if (stack!=null && cm!=null)
            stack.setColorModel(cm);
        return stack;
    }

    /** Returns an ImageProcessor for the specified frame of this virtual stack (if it is one)
        where 1<=n<=nslices. Returns null if no virtual stack or no slices.
    */
    public synchronized ImageProcessor getProcessor(int n) {
        if (frameInfos==null || frameInfos.size()==0 || raFilePath==null)
            return null;
        if (n<1 || n>frameInfos.size())
            throw new IllegalArgumentException("Argument out of range: "+n);
        Object pixels = null;
        RandomAccessFile rFile = null;
        String exceptionMessage = null;
        try {
            rFile = new RandomAccessFile(new File(raFilePath), "r");
            long[] frameInfo = (long[])(frameInfos.get(n-1));
            pixels = readFrame(rFile, frameInfo[0], (int)frameInfo[1]);
        } catch (Exception e) {
            exceptionMessage = exceptionMessage(e);
        } finally {
            try {
                rFile.close();
            } catch (Exception e) {}
        }
        if (exceptionMessage != null) throw new RuntimeException(exceptionMessage);
        if (pixels == null) return null; //failed
        if (pixels instanceof byte[])
            return new ByteProcessor(dwWidth, biHeight, (byte[])pixels, cm);
        else if (pixels instanceof short[])
            return new ShortProcessor(dwWidth, biHeight, (short[])pixels, cm);
        else
            return new ColorProcessor(dwWidth, biHeight, (int[])pixels);
    }
 
    /** Returns the image width of the virtual stack */
    public int getWidth() {
        return dwWidth;
    }

    /** Returns the image height of the virtual stack */
    public int getHeight() {
        return biHeight;
    }

    /** Returns the number of images in this virtual stack (if it is one) */
    public int getSize() {
        if (frameInfos == null) return 0;
        else return frameInfos.size();
    }

    /** Returns the label of the specified slice in this virtual stack (if it is one). */
    public String getSliceLabel(int n) {
        if (frameInfos==null || n<1 || n>frameInfos.size())
            throw new IllegalArgumentException("No Virtual Stack or argument out of range: "+n);
        return frameLabel(((long[])(frameInfos.get(n-1)))[2]);
    }

    /** Deletes the specified image from this virtual stack (if it is one),
     *  where 1<=n<=nslices. */
    public void deleteSlice(int n) {
        if (frameInfos==null || frameInfos.size()==0) return;
        if (n<1 || n>frameInfos.size())
            throw new IllegalArgumentException("Argument out of range: "+n);
        frameInfos.removeElementAt(n-1);
    }

    /** Parameters dialog, returns false on cancel */
    private boolean showDialog (String fileName) {
    	if (lastFrameNumber!=-1)
    		lastFrameNumber = dwTotalFrames;
    	if (IJ.macroRunning()) {
    		firstFrameNumber = 1;
    		lastFrameNumber = dwTotalFrames;
    	}
        GenericDialog gd = new GenericDialog("AVI Reader");
        gd.addNumericField("First Frame: ", firstFrameNumber, 0);
        gd.addNumericField("Last Frame: ", lastFrameNumber, 0, 6, "");
        gd.addCheckbox("Use Virtual Stack", isVirtual);
        gd.addCheckbox("Convert to Grayscale", convertToGray);
        gd.addCheckbox("Flip Vertical", flipVertical);
        gd.showDialog();
        if (gd.wasCanceled()) return false;
        firstFrameNumber = (int)gd.getNextNumber();
        lastFrameNumber = (int)gd.getNextNumber();
        isVirtual = gd.getNextBoolean();
        convertToGray = gd.getNextBoolean();
        flipVertical = gd.getNextBoolean();
        IJ.register(this.getClass());
        return true;
    }

    private void readAVI(String path) throws Exception, IOException {
        if (!headerOK)                          //we have not read the header yet?
            openAndReadHeader(path);
        startTime += System.currentTimeMillis();//taking previously elapsed time into account
        findFourccAndRead(FOURCC_movi, true, fileSize, true); //read movie data
        return;
    }

    /** Open the file with given path and read its header */
    private void openAndReadHeader (String path) throws Exception, IOException {
        startTime = System.currentTimeMillis();
        if (verbose)
            IJ.log("OPEN AND READ AVI FILE HEADER "+timeString());
        File file = new File(path);                         // o p e n
        raFile = new RandomAccessFile(file, "r");
        raFilePath = path;
        fileSize = raFile.length();
        int fileType = readInt();                           // f i l e   h e a d e r
        if (verbose)
            IJ.log("File header: File type='"+fourccString(fileType)+"' (should be 'RIFF')"+timeString());
        if (fileType != FOURCC_RIFF)
            throw new Exception("Not an AVI file.");
        readInt(); //data size
        int riffType = readInt();
        if (verbose)
            IJ.log("File header: RIFF type='"+fourccString(riffType)+"' (should be 'AVI ')");
        if (riffType != FOURCC_AVI)
            throw new Exception("Not an AVI file.");
        findFourccAndRead(FOURCC_hdrl, true, fileSize, true);
        startTime -= System.currentTimeMillis(); //becomes minus elapsed Time
        headerOK = true;
    }

    /** Find the next position of fourcc or LIST fourcc and read contents.
     *  Returns next position
     *  If not found, throws exception or returns -1 */
    private long findFourccAndRead(int fourcc, boolean isList, long endPosition,
            boolean throwNotFoundException) throws Exception, IOException {
        long nextPos;
        boolean contentOk = false;
        do {
            int type = readType(endPosition);
            if (type == 0) {            //reached endPosition without finding
                if (throwNotFoundException)
                    throw new Exception("Required item '"+fourccString(fourcc)+"' not found");
                else
                    return -1;
            }
            long size = readInt() & SIZE_MASK;
            nextPos = raFile.getFilePointer() + size;
            boolean foundList = false;
            if (isList && type == FOURCC_LIST) {
                foundList = true;
                type = readInt();
            }
            if (verbose)
                IJ.log("Searching for '"+fourccString(fourcc)+"', found "+(foundList?"LIST '":"'")
                    +fourccString(type)+"' "+posSizeString(nextPos-size, size));
            if (type==fourcc) {
                contentOk = readContents(fourcc, nextPos);
            } else if (verbose)
                IJ.log("Discarded '"+fourccString(type)+"': Contents does not fit");
            raFile.seek(nextPos);
            if (contentOk)
                return nextPos;     //found and read, breaks the loop
        } while (!contentOk);
        return nextPos;
    }

    /** read contents defined by four-cc code; returns true if contens ok */
    private boolean readContents (int fourcc, long endPosition) throws Exception, IOException {
        switch (fourcc) {
            case FOURCC_hdrl:
                findFourccAndRead(FOURCC_avih, false, endPosition, true);
                findFourccAndRead(FOURCC_strl, true, endPosition, true);
                return true;
            case FOURCC_avih:
                readAviHeader();
                return true;
            case FOURCC_strl:
                long nextPosition = findFourccAndRead(FOURCC_strh, false, endPosition, false);
                if (nextPosition<0) return false;
                findFourccAndRead(FOURCC_strf, false, endPosition, true);
                return true;
            case FOURCC_strh:
                int streamType = readInt();
                if (streamType != FOURCC_vids) {
                    if (verbose)
                        IJ.log("Non-video Stream '"+fourccString(streamType)+" skipped");
                    streamNumber++;
                    return false;
                }
                readStreamHeader();
                return true;
            case FOURCC_strf:
                readBitMapInfo(endPosition);
                return true;
            case FOURCC_movi:
                readMovieData(endPosition);
                return true;
        }
        throw new Exception("Program error, type "+fourccString(fourcc));
    }

    void readAviHeader() throws Exception, IOException {
        dwMicroSecPerFrame = readInt();
        dwMaxBytesPerSec = readInt();
        dwReserved1 = readInt(); //in newer avi formats, this is dwPaddingGranularity?
        dwFlags = readInt();
        dwTotalFrames = readInt();
        dwInitialFrames = readInt();
        dwStreams = readInt();
        dwSuggestedBufferSize = readInt();
        dwWidth = readInt();
        dwHeight = readInt();
        // dwReserved[4] follows, ignored

        if (verbose) {
            IJ.log("AVI HEADER (avih):"+timeString());
            IJ.log("   dwMicroSecPerFrame=" + dwMicroSecPerFrame);
            IJ.log("   dwMaxBytesPerSec=" + dwMaxBytesPerSec);
            IJ.log("   dwReserved1=" + dwReserved1);
            IJ.log("   dwFlags=" + dwFlags);
            IJ.log("   dwTotalFrames=" + dwTotalFrames);
            IJ.log("   dwInitialFrames=" + dwInitialFrames);
            IJ.log("   dwStreams=" + dwStreams);
            IJ.log("   dwSuggestedBufferSize=" + dwSuggestedBufferSize);
            IJ.log("   dwWidth=" + dwWidth);
            IJ.log("   dwHeight=" + dwHeight);
        }
    }

    void readStreamHeader() throws Exception, IOException {
        fccStreamHandler = readInt();
        dwStreamFlags = readInt();
        dwPriorityLanguage = readInt();
        dwStreamInitialFrames = readInt();
        dwStreamScale = readInt();
        dwStreamRate = readInt();
        dwStreamStart = readInt();
        dwStreamLength = readInt();
        dwStreamSuggestedBufferSize = readInt();
        dwStreamQuality = readInt();
        dwStreamSampleSize = readInt();
        //rcFrame rectangle follows, ignored
        if (verbose) {
            IJ.log("VIDEO STREAM HEADER (strh):");
            IJ.log("   fccStreamHandler='" + fourccString(fccStreamHandler)+"'");
            IJ.log("   dwStreamFlags=" + dwStreamFlags);
            IJ.log("   wPriority,wLanguage=" + dwPriorityLanguage);
            IJ.log("   dwStreamInitialFrames=" + dwStreamInitialFrames);
            IJ.log("   dwStreamScale=" + dwStreamScale);
            IJ.log("   dwStreamRate=" + dwStreamRate);
            IJ.log("   dwStreamStart=" + dwStreamStart);
            IJ.log("   dwStreamLength=" + dwStreamLength);
            IJ.log("   dwStreamSuggestedBufferSize=" + dwStreamSuggestedBufferSize);
            IJ.log("   dwStreamQuality=" + dwStreamQuality);
            IJ.log("   dwStreamSampleSize=" + dwStreamSampleSize);
        }
        if (dwStreamSampleSize > 1)
            throw new Exception("Video stream with "+dwStreamSampleSize+" (more than 1) frames/chunk not supported");
    }

    /**Read stream format chunk: starts with BitMapInfo, may contain palette
    */
    void readBitMapInfo(long endPosition) throws Exception, IOException {
        biSize = readInt();
        biWidth = readInt();
        biHeight = readInt();
        biPlanes = readShort();
        biBitCount = readShort();
        biCompression = readInt();
        biSizeImage = readInt();
        biXPelsPerMeter = readInt();
        biYPelsPerMeter = readInt();
        biClrUsed = readInt();
        biClrImportant = readInt();
        if (verbose) {
            IJ.log("   biSize=" + biSize);
            IJ.log("   biWidth=" + biWidth);
            IJ.log("   biHeight=" + biHeight);
            IJ.log("   biPlanes=" + biPlanes);
            IJ.log("   biBitCount=" + biBitCount);
            IJ.log("   biCompression=0x" + Integer.toHexString(biCompression)+" '"+fourccString(biCompression)+"'");
            IJ.log("   biSizeImage=" + biSizeImage);
            IJ.log("   biXPelsPerMeter=" + biXPelsPerMeter);
            IJ.log("   biYPelsPerMeter=" + biYPelsPerMeter);
            IJ.log("   biClrUsed=" + biClrUsed);
            IJ.log("   biClrImportant=" + biClrImportant);
        }

        int allowedBitCount = 0;
        boolean readPalette = false;
        switch (biCompression) {
            case NO_COMPRESSION:
            case NO_COMPRESSION_RGB:
            case NO_COMPRESSION_RAW:
                dataCompression = NO_COMPRESSION;
                dataTopDown = biHeight<0;   //RGB mode is usually bottom-up, negative height signals top-down
                allowedBitCount = 8 | BITMASK24 | 32; //we don't support 1, 2 and 4 byte data
                readPalette = biBitCount <= 8;
                break;
            case NO_COMPRESSION_Y8:
            case NO_COMPRESSION_GREY:
            case NO_COMPRESSION_Y800:
                dataTopDown = true;
                dataCompression = NO_COMPRESSION;
                allowedBitCount = 8;
                break;
            case NO_COMPRESSION_Y16:
            case NO_COMPRESSION_MIL:
                dataCompression = NO_COMPRESSION;
                allowedBitCount = 16;
                break;
            case AYUV_COMPRESSION:
                dataCompression = AYUV_COMPRESSION;
                allowedBitCount = 32;
                break;
            case UYVY_COMPRESSION:
            case UYNV_COMPRESSION:
                dataTopDown = true;
            case CYUV_COMPRESSION:  //same, not top-down
            case V422_COMPRESSION:
                dataCompression = UYVY_COMPRESSION;
                allowedBitCount = 16;
                break;
            case YUY2_COMPRESSION:
            case YUNV_COMPRESSION:
            case YUYV_COMPRESSION:
                dataTopDown = true;
                dataCompression = YUY2_COMPRESSION;
                allowedBitCount = 16;
                break;
            case YVYU_COMPRESSION:
                dataTopDown = true;
                dataCompression = YVYU_COMPRESSION;
                allowedBitCount = 16;
                break;
            case JPEG_COMPRESSION:
            case JPEG_COMPRESSION2:
            case JPEG_COMPRESSION3:
            case MJPG_COMPRESSION:
                dataCompression = JPEG_COMPRESSION;
                variableLength = true;
                break;
            case PNG_COMPRESSION:
            case PNG_COMPRESSION2:
            case PNG_COMPRESSION3:
                variableLength = true;
                dataCompression = PNG_COMPRESSION;
                break;
            default:
                throw new Exception("Unsupported compression: "+Integer.toHexString(biCompression)+
                        (biCompression>=0x20202020 ? " '" + fourccString(biCompression)+"'" : ""));
        }

        int bitCountTest = biBitCount==24 ? BITMASK24 : biBitCount;  //convert "24" to a flag
        if (allowedBitCount!=0 && (bitCountTest & allowedBitCount)==0)
            throw new Exception("Unsupported: "+biBitCount+" bits/pixel for compression '"+
                    fourccString(biCompression)+"'");

        if (biHeight < 0)       //negative height was for top-down data in RGB mode
            biHeight = -biHeight;

        // Scan line is padded with zeroes to be a multiple of four bytes
        scanLineSize = ((biWidth * biBitCount + 31) / 32) * 4;

        // a value of biClrUsed=0 means we determine this based on the bits per pixel
        if (readPalette && biClrUsed==0)
            biClrUsed = 1 << biBitCount;

        if (verbose) {
            IJ.log("   > data compression=0x" + Integer.toHexString(dataCompression)+" '"+fourccString(dataCompression)+"'");
            IJ.log("   > palette colors=" + biClrUsed);
            IJ.log("   > scan line size=" + scanLineSize);
            IJ.log("   > data top down=" + dataTopDown);
        }

        //read color palette
        if (readPalette) {
            long spaceForPalette  = endPosition-raFile.getFilePointer();
            if (verbose)
                IJ.log("   Reading "+biClrUsed+" Palette colors: " + posSizeString(spaceForPalette));
            if (spaceForPalette < biClrUsed*4)
                throw new Exception("Not enough data ("+spaceForPalette+") for palette of size "+(biClrUsed*4));
            byte[]  pr    = new byte[biClrUsed];
            byte[]  pg    = new byte[biClrUsed];
            byte[]  pb    = new byte[biClrUsed];
            for (int i = 0; i < biClrUsed; i++) {
                pb[i] = raFile.readByte();
                pg[i] = raFile.readByte();
                pr[i] = raFile.readByte();
                raFile.readByte();
            }
            cm = new IndexColorModel(biBitCount, biClrUsed, pr, pg, pb);
        }
    }

    /**Read from the 'movi' chunk. Skips audio ('..wb', etc.), 'LIST' 'rec' etc, only reads '..db' or '..dc'*/
    void readMovieData(long endPosition) throws Exception, IOException {
        if (verbose)
            IJ.log("MOVIE DATA "+posSizeString(endPosition-raFile.getFilePointer())+timeString());
        //prepare for reading
        int type0xdb = FOURCC_00db + (streamNumber<<8);  //'01db' for stream 1, etc. (inverse byte order!)
        int type0xdc = FOURCC_00dc + (streamNumber<<8);  //'01dc' for stream 1, etc.
        if (verbose)
            IJ.log("Searching for stream "+streamNumber+": '"+
                    fourccString(type0xdb)+"' or '"+fourccString(type0xdc)+"' chunks");
        int lastFrameToRead = Integer.MAX_VALUE;
        if (lastFrameNumber > 0)            //last frame number to read: from Dialog
            lastFrameToRead = lastFrameNumber;
        if (lastFrameNumber < 0 && dwTotalFrames > 0) //negative means "end frame minus ..."
            lastFrameToRead = dwTotalFrames+lastFrameNumber;
        if (isVirtual)
            frameInfos = new Vector(100);    //holds frame positions in file (for non-constant frame sizes, should hold long[] with pos and size)
        else
            stack = new ImageStack(dwWidth, biHeight);
        int frameNumber = 1;
        while (true) {                          //loop over all chunks
            int type = readType(endPosition);
            if (type==0) break;                 //end of 'movi' reached?
            long size = readInt() & SIZE_MASK;
            long pos = raFile.getFilePointer();
            long nextPos = pos + size;
            if ((type==type0xdb || type==type0xdc) && size>0) {
                updateProgress();
                if (verbose)
                    IJ.log("movie data '"+fourccString(type)+"' "+posSizeString(size)+timeString());
                if (frameNumber >= firstFrameNumber) {
                    if (isVirtual)
                        frameInfos.add(new long[]{pos, size, frameNumber*dwMicroSecPerFrame});
                    else {                       //read the frame
                        Object pixels = readFrame(raFile, pos, (int)size);
                        String label = frameLabel(frameNumber*dwMicroSecPerFrame);
                        stack.addSlice(label, pixels);
                    }
                }
                frameNumber++;
                if (frameNumber>lastFrameToRead) break;
            } else if (verbose)
                IJ.log("skipped '"+fourccString(type)+"' "+posSizeString(size));
            if (nextPos > endPosition) break;
            raFile.seek(nextPos);
        }
    }

    /** Reads a frame at a given position in the file, returns pixels array */
    private Object readFrame (RandomAccessFile rFile, long filePos, int size)
            throws Exception, IOException {
        rFile.seek(filePos);
        if (variableLength)                 //JPEG or PNG-compressed frames
            return readCompressedFrame(rFile, size);
        else
            return readFixedLengthFrame(rFile, size);
    }

    /** Reads a JPEG or PNG-compressed frame from a RandomAccessFile and
     *  returns the pixels array of the resulting image abd sets the
     *  ColorModel cm (if appropriate) */
    private Object readCompressedFrame (RandomAccessFile rFile, int size)
            throws Exception, IOException {
        InputStream inputStream = new raInputStream(rFile, size, biCompression==MJPG_COMPRESSION);
        BufferedImage bi = ImageIO.read(inputStream);
        int type = bi.getType();
        ImageProcessor ip = null;
        //IJ.log("BufferedImage Type="+type);
        if (type==BufferedImage.TYPE_BYTE_GRAY) {
            ip = new ByteProcessor(bi);
        } else if (type==bi.TYPE_BYTE_INDEXED) {
            cm = bi.getColorModel();
            ip = new ByteProcessor((Image)bi);
        } else
            ip =  new ColorProcessor(bi);
        if (convertToGray)
            ip = ip.convertToByte(false);
        if (flipVertical)
            ip.flipVertical();
        return ip.getPixels();
    }

    /** Read a fixed-length frame (RandomAccessFile rFile, long filePos, int size)
     *  return the pixels array of the resulting image
     */
    private Object readFixedLengthFrame (RandomAccessFile rFile, int size)  throws Exception, IOException {
         if (size < scanLineSize*biHeight) 
            throw new Exception("Data chunk size "+size+" too short ("+(scanLineSize*biHeight)+" required)");
        byte[] rawData = new byte[size];
        int  n  = rFile.read(rawData, 0, size);
        if (n < rawData.length)
            throw new Exception("Frame ended prematurely after " + n + " bytes");
        boolean topDown = flipVertical ? !dataTopDown : dataTopDown;
        Object pixels = null;
        byte[] bPixels = null;
        int[] cPixels = null;
        short[] sPixels = null;
        if (biBitCount <=8 || convertToGray) {
            bPixels = new byte[dwWidth * biHeight];
            pixels = bPixels;
        } else if (biBitCount == 16 && dataCompression == NO_COMPRESSION) {
            sPixels = new short[dwWidth * biHeight];
            pixels = sPixels;
        } else {
            cPixels = new int[dwWidth * biHeight];
            pixels = cPixels;
        }
        int  offset     = topDown ? 0 : (biHeight-1)*dwWidth;
        int  rawOffset  = 0;
        for (int i = biHeight - 1; i >= 0; i--) {  //for all lines
            if (biBitCount <=8)
                unpack8bit(rawData, rawOffset, bPixels, offset, dwWidth);
            else if (convertToGray)
                unpackGray(rawData, rawOffset, bPixels, offset, dwWidth);
            else if (biBitCount==16 && dataCompression == NO_COMPRESSION)
                unpackShort(rawData, rawOffset, sPixels, offset, dwWidth);
            else
                unpack(rawData, rawOffset, cPixels, offset, dwWidth);
            rawOffset += scanLineSize;
            offset += topDown ? dwWidth : -dwWidth;
        }
        return pixels;
    }

    /** For one line: copy byte data into the byte array for creating a ByteProcessor */
    void unpack8bit(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) {
        for (int i = 0; i < w; i++)
            pixels[byteOffset + i] = rawData[rawOffset + i];
    }

    /** For one line: Unpack and convert YUV or RGB video data to grayscale (byte array for ByteProcessor) */
    void unpackGray(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) {
        int  j     = byteOffset;
        int  k     = rawOffset;
        if (dataCompression == 0) {
            for (int i = 0; i < w; i++) {
                int  b0  = (((int) (rawData[k++])) & 0xff);
                int  b1  = (((int) (rawData[k++])) & 0xff);
                int  b2  = (((int) (rawData[k++])) & 0xff);
                if (biBitCount==32) k++; // ignore 4th byte (alpha value) 
                pixels[j++] = (byte)((b0*934 + b1*4809 + b2*2449 + 4096)>>13); //0.299*R+0.587*G+0.114*B
            }
        } else {
            if (dataCompression==UYVY_COMPRESSION || dataCompression==AYUV_COMPRESSION)
                k++; //skip first byte in these formats (chroma)
            int step = dataCompression==AYUV_COMPRESSION ? 4 : 2;
            for (int i = 0; i < w; i++) {
                pixels[j++] = rawData[k];   //Non-standard: no scaling from 16-235 to 0-255 here
                k+=step;
            }
        }
    }

    /** For one line: Unpack 16bit grayscale data and convert to short array for ShortProcessor */
    void unpackShort(byte[] rawData, int rawOffset, short[] pixels, int shortOffset, int w) {
        int  j     = shortOffset;
        int  k     = rawOffset;
        for (int i = 0; i < w; i++) {
            pixels[j++] = (short) ((int)(rawData[k++] & 0xFF)| (((int)(rawData[k++] & 0xFF))<<8));
        }
    }

    /** For one line: Read YUV, RGB or RGB+alpha data and writes RGB int array for ColorProcessor */
    void unpack(byte[] rawData, int rawOffset, int[] pixels, int intOffset, int w) {
        int  j     = intOffset;
        int  k     = rawOffset;
        switch (dataCompression) {
            case NO_COMPRESSION:
                for (int i = 0; i < w; i++) {
                    int  b0  = (((int) (rawData[k++])) & 0xff);
                    int  b1  = (((int) (rawData[k++])) & 0xff) << 8;
                    int  b2  = (((int) (rawData[k++])) & 0xff) << 16;
                    if (biBitCount==32) k++; // ignore 4th byte (alpha value) 
                    pixels[j++] = 0xff000000 | b0 | b1 | b2;
                }
                break;
            case YUY2_COMPRESSION:
                for (int i = 0; i < w/2; i++) {
                    int y0 = rawData[k++] & 0xff;
                    int u  = rawData[k++] ^ 0xffffff80; //converts byte range 0...ff to -128 ... 127
                    int y1 = rawData[k++] & 0xff;
                    int v  = rawData[k++] ^ 0xffffff80;
                    writeRGBfromYUV(y0, u, v, pixels, j++);
                    writeRGBfromYUV(y1, u, v, pixels, j++);
                }
                break;
            case UYVY_COMPRESSION:
                for (int i = 0; i < w/2; i++) {
                    int u  = rawData[k++] ^ 0xffffff80;
                    int y0 = rawData[k++] & 0xff;
                    int v  = rawData[k++] ^ 0xffffff80;
                    int y1 = rawData[k++] & 0xff;
                    writeRGBfromYUV(y0, u, v, pixels, j++);
                    writeRGBfromYUV(y1, u, v, pixels, j++);
                }
                break;
            case YVYU_COMPRESSION:
                for (int i = 0; i < w/2; i++) {
                    int y0 = rawData[k++] & 0xff;
                    int v  = rawData[k++] ^ 0xffffff80;
                    int y1 = rawData[k++] & 0xff;
                    int u  = rawData[k++] ^ 0xffffff80;
                    writeRGBfromYUV(y0, u, v, pixels, j++);
                    writeRGBfromYUV(y1, u, v, pixels, j++);
                }
                break;
            case AYUV_COMPRESSION:
                for (int i = 0; i < w; i++) {
                    k++;    //ignore alpha channel
                    int y  = rawData[k++] & 0xff;
                    int v  = rawData[k++] ^ 0xffffff80;
                    int u  = rawData[k++] ^ 0xffffff80;
                    writeRGBfromYUV(y, u, v, pixels, j++);
                }
                break;
                
        }
    }
    
    /** Write an intData RGB value converted from YUV,
     *  The y range between 16 and 235 becomes 0...255
     *  u, v should be between -112 and +112
     */
    void writeRGBfromYUV(int y, int u, int v, int[]pixels, int intArrayIndex) {
        //int r = (int)(1.164*(y-16)+1.596*v+0.5);
        //int g = (int)(1.164*(y-16)-0.391*u-0.813*v+0.5);
        //int b = (int)(1.164*(y-16)+2.018*u+0.5);
        int r = (9535*y + 13074*v -148464) >> 13;
        int g = (9535*y - 6660*v - 3203*u -148464) >> 13;
        int b = (9535*y + 16531*u -148464) >> 13;
        if (r>255) r=255; if (r<0) r=0;
        if (g>255) g=255; if (g<0) g=0;
        if (b>255) b=255; if (b<0) b=0;
        pixels[intArrayIndex] = 0xff000000 | (r<<16) | (g<<8) | b;
    }

    /** Read 4-byte int with Intel (little-endian) byte order
     * (note: RandomAccessFile.readInt has other byte order than AVI) */
    int readInt() throws IOException {
        int  result = 0;
        for (int shiftBy = 0; shiftBy < 32; shiftBy += 8)
            result |= (raFile.readByte() & 0xff) << shiftBy;
        return result;
    }

    /** Read 2-byte short with Intel (little-endian) byte order
     * (note: RandomAccessFile.readShort has other byte order than AVI) */
    short readShort() throws IOException {
        int  low   = raFile.readByte() & 0xff;
        int  high  = raFile.readByte() & 0xff;
        return (short) (high << 8 | low);
    }

    /** Read type of next chunk that is not JUNK.
     *  Returns type (or 0 if no non-JUNK chunk until endPosition) */
    private int readType(long endPosition) throws IOException {
        while (true) {
            long pos = raFile.getFilePointer();
            if (pos%paddingGranularity!=0) {
                pos = (pos/paddingGranularity+1)*paddingGranularity;
                raFile.seek(pos);    //pad to even address
            }
            if (pos >= endPosition) return 0;
            int type = readInt();
            if (type != FOURCC_JUNK)
                return type;
            long size = readInt()&SIZE_MASK;
            if (verbose)
                IJ.log("Skip JUNK: "+posSizeString(size));
            raFile.seek(raFile.getFilePointer()+size);  //skip junk
        }
    }

    private void setFramesPerSecond (ImagePlus imp) {
        if (dwMicroSecPerFrame<1000 && dwStreamRate>0)  //if no reasonable frame time, get it from rate
            dwMicroSecPerFrame = (int)(dwStreamScale*1e6/dwStreamRate);
        if (dwMicroSecPerFrame>=1000)
            imp.getCalibration().fps = 1e6 / dwMicroSecPerFrame;
    }

    private String frameLabel(long timeMicroSec) {
        return IJ.d2s(timeMicroSec/1.e6)+" s";
    }

    private String posSizeString(long size) throws IOException {
        return posSizeString(raFile.getFilePointer(), size);
    }

    private String posSizeString(long pos, long size) throws IOException {
        return "0x"+Long.toHexString(pos)+"-0x"+Long.toHexString(pos+size-1)+" ("+size+" Bytes)";
    }

    private String timeString() {
        return " (t="+(System.currentTimeMillis()-startTime)+" ms)";
    }

    /** returns a string of a four-cc code corresponding to an int (Intel byte order) */
    private String fourccString(int fourcc) {
        String s = "";
        for (int i=0; i<4; i++) {
            int c = fourcc&0xff;
            s += Character.toString((char)c);
            fourcc >>= 8;
        }
        return s;
    }

    private String exceptionMessage (Exception e) {
        String  msg;
        if (e.getClass() == Exception.class)    //for "home-built" exceptions: message only
            msg = e.getMessage();
        else
            msg = e + "\n" + e.getStackTrace()[0]+"\n"+e.getStackTrace()[1];
        if (msg.indexOf("Huffman table")!=-1)
            return "Cannot open M_JPEG AVIs that are missing Huffman tables";
        else
           return "An error occurred reading the file.\n \n" + msg;
    }

    void updateProgress() throws IOException {
        IJ.showProgress((double) raFile.getFilePointer() / fileSize);
    }

    /** An input stream reading from a RandomAccessFile (starting at the current position)
     *  This class contains a hack to convert a 'Define Huffman Table' (DHT) segments
     *  from MJPG to JPEG.
     *  The hack works only if the first bytes are read in a sufficiently large
     *  array to include the start of the DHT segment.
     */
    class raInputStream extends InputStream {
        RandomAccessFile rFile; //where to read the data from
        int readableSize;       //number of bytes that one should expect to be readable
        boolean fixMJPG;        //whether to use an ugly hack to convert MJPG frames to JPEG

        /** Constructor */
        raInputStream (RandomAccessFile rFile, int readableSize, boolean fixMJPG) {
            this.rFile = rFile;
            this.readableSize = readableSize;
            this.fixMJPG = fixMJPG;
        }
        public int available () {
            return readableSize;
        }
        // Read methods.
        // Reading beyond start position + readableSize (i.e. beyond the frame in the avi file)
        // is possible - no checks are done (as long as it is within the bounds of the file).
        public int read () throws IOException {
            fixMJPG = false;    //the fix won't work after reading single bytes
            return rFile.read();
        }
        public int read (byte[] b) throws IOException {
            return read(b, 0, b.length);
        }
        public int read (byte[] b, int off, int len) throws IOException {
            int nBytes = rFile.read(b, off, len);
            if (fixMJPG) {
                doFixMJPG(b, nBytes);
                fixMJPG = false;
            }
            return nBytes;
        }
        // A Hack: Replace 0xffb3 segment markers with 0xffc4, the JPEG code for
        // 'Define Huffman Table' (DHT)
        // This makes at least some MJPG files readable by the JPEG decoder
        private void doFixMJPG (byte[] b, int len) {
            //IJ.log("data code="+Integer.toHexString(readShort(b, 0)));
            if (readShort(b, 0)!=0xffd8 || len<6) return;   //not a start of JPEG-like data
            int offset = 2;
            while (true) {
                int code = readShort(b, offset);            //read segment type
                if (code==0xffda || code==0xffd9) return;   //start of image data or end
                if (code==0xffe2) {
                    b[offset+1] = (byte)0xc4;
                    return;                                 //fixed
                }
                offset += 2;
                int segmentLength = readShort(b, offset);
                offset += segmentLength;
                if (offset>len-4 || segmentLength<0) return;//pointed out of file
            }
        }
        private int readShort(byte[] b, int offset) {
            return ((b[offset]&0xff)<<8) | (b[offset+1]&0xff);
        }
    }
}
